﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace SharpDepend.Shared
{
    internal class ClientManager : ISocketClientManager
    {
        internal class Client : ISocketClient
        {
            // Signaling object used to notify when an asynchronous operation is completed
            static ManualResetEvent mLockerSend = new ManualResetEvent(false);
            static ManualResetEvent mLockerReceive = new ManualResetEvent(false);
            static ManualResetEvent mLockerConnect = new ManualResetEvent(false);

            private Socket mClient;

            SocketAsyncEventArgs mSendEvents;
            SocketAsyncEventArgs mReceiveEvents;
            SocketAsyncEventArgs mConnectEvents;

            private byte[] mData;
            private int mOffset;
            private int mLength;
            private int mReaded;

            // Define a timeout in milliseconds for each asynchronous call. If a response is not received within this 
            // timeout period, the call is aborted.
            const int TIMEOUT_MILLISECONDS = 60000 * 5;

            const int TIMEOUT_RECEIVED_MILLISECONDS = 60000 * 2;

            // The maximum size of the data buffer to use with the asynchronous socket methods
            const int MAX_BUFFER_SIZE = 2048;

            public bool IsConnected
            {
                get { return mClient.Connected; }
            }

            public Client()
            {
                mClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                Initialize();
            }

            public Client(Socket client)
            {
                mClient = client;
                Initialize();
            }

            private void Initialize()
            {
                mClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                mSendEvents = new SocketAsyncEventArgs();
                mReceiveEvents = new SocketAsyncEventArgs();
                mConnectEvents = new SocketAsyncEventArgs();

                // Inline event handler for the Completed event.
                // Note: This event handler was implemented inline in order 
                // to make this method self-contained.
                mSendEvents.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs e)
                {
                    // Unblock the UI thread
                    mLockerSend.Set();
                });

                // Inline event handler for the Completed event.
                // Note: This even handler was implemented inline in order to make 
                // this method self-contained.
                mReceiveEvents.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs e)
                {
                    if (e.SocketError == SocketError.Success)
                    {
                        // Retrieve the data from the buffer
                        mData = e.Buffer;
                        mOffset = e.Offset;
                        mLength = e.BytesTransferred;
                    }
                    else
                    {

                    }

                    mLockerReceive.Set();
                });

                // Inline event handler for the Completed event.
                // Note: This event handler was implemented inline in order to make this method self-contained.
                mConnectEvents.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs e)
                {
                    mLockerConnect.Set();
                });
            }

            public void Connect(string ip, int port)
            {
                // Create DnsEndPoint. The hostName and port are passed in to this method.
                DnsEndPoint hostEntry = new DnsEndPoint(ip, port);

                // Create a SocketAsyncEventArgs object to be used in the connection request.
                mConnectEvents.RemoteEndPoint = hostEntry;

                mLockerConnect.Reset();

                // Make an asynchronous Connect request over the socket.
                mClient.ConnectAsync(mConnectEvents);

                // Block the UI thread for a maximum of TIMEOUT_MILLISECONDS milliseconds.
                // If no response comes back within this time then proceed
                if (!mLockerConnect.WaitOne(TIMEOUT_MILLISECONDS))
                {
                    throw new TimeoutException("Connection timeout.");
                }
            }

            public void Write(byte[] data, int offset, int length)
            {
                // Set properties on context object
                mSendEvents.RemoteEndPoint = mClient.RemoteEndPoint;
                mSendEvents.UserToken = null;

                // Add the data to be sent into the buffer
                mSendEvents.SetBuffer(data, offset, length);

                // Sets the state of the event to nonsignaled, causing threads to block
                mLockerSend.Reset();

                // Make an asynchronous Send request over the socket
                mClient.SendAsync(mSendEvents);

                // Block the UI thread for a maximum of TIMEOUT_MILLISECONDS milliseconds.
                // If no response comes back within this time then proceed
                mLockerSend.WaitOne(TIMEOUT_MILLISECONDS);
            }

            public int GetAvailable()
            {
                ReceiveDataIfAvailable();

                return mLength - mReaded;
            }

            public int ReadByte()
            {
                ReceiveDataIfAvailable();

                ++mReaded;
                int output = mData[0];

                CheckFinished();

                return output;
            }

            public void Read(byte[] data, int offset, int length)
            {
                ReceiveDataIfAvailable();

                Array.Copy(mData, mReaded, data, offset, length);

                mReaded += length;

                CheckFinished();
            }

            private void CheckFinished()
            {
                if (mReaded == mLength)
                {
                    mData = null;
                    mReaded = 0;
                }
            }

            private bool ReceiveDataIfAvailable()
            {
                if (mData == null)
                {
                    // Create SocketAsyncEventArgs context object
                    mReceiveEvents.RemoteEndPoint = mClient.RemoteEndPoint;

                    // Setup the buffer to receive the data
                    mReceiveEvents.SetBuffer(new byte[MAX_BUFFER_SIZE], 0, MAX_BUFFER_SIZE);

                    // Sets the state of the event to nonsignaled, causing threads to block
                    mLockerReceive.Reset();

                    // Make an asynchronous Receive request over the socket
                    mClient.ReceiveAsync(mReceiveEvents);

                    // Block the UI thread for a maximum of TIMEOUT_MILLISECONDS milliseconds.
                    // If no response comes back within this time then proceed
                    if (!mLockerReceive.WaitOne(TIMEOUT_MILLISECONDS))
                    {
                        throw new TimeoutException("Receive socket timeout.");
                    }

                    if (mData == null)
                    {
                        throw new Exception("Unavailable to get data.");
                    }

                    return true;
                }
                else
                {
                    return false;
                }
            }

            public void Close()
            {
                mClient.Close();
            }

            public string IP
            {
                get
                {
                    return ((IPEndPoint)mClient.RemoteEndPoint).Address.ToString();
                }
            }
        }

        public ISocketClient CreateClient()
        {
            return new Client();
        }
    }
}
